//
// Created by haoy on 2018/1/2.
//

#include "mixer_manager.hpp"

#include <map>
#include <sstream>

#include "xm_app_config.h"
#include "rtc_recorder.h"
#include "xm_log.h"
#include "xmixer.h"

typedef struct {
    std::string sceneName;
    std::string sourceName;
    std::string internalSourceName;
    std::string segmentName;
} MixerRecordingInfo;

// MixerManager could be merged into rtc_manager,
// but since the rtc_manager is a C struct,
// using stl in a C struct requires extra new/delete,
// so, it is leaved here.
// When we are ready to factor rtc_manager into c++,
// we can merge MixerManager into rtc_manager.
// todo: merge MixerManager into rtc_manager
class MixerManager {
public:
    MixerManager(GMainContext* main_context, rtc_recorder_t recorder)
            : recorder_(recorder)
    {
        timer_ = g_timeout_source_new(20);
        g_source_set_callback(timer_, &MixerManager::on_tick, this, nullptr);
        g_source_attach(timer_, main_context);
    }

    static void on_rtc_destroy(rtc_endpoint_t rtc, void* mixer_manager);
    static gboolean on_tick(gpointer mixer_manager);

    std::map<std::string, xmixer_t> mixers_; // mixers by id
    std::map<rtc_endpoint_t, xmixer_t> rtc_to_mixer_; // this prevent rtc added to 2nd mixer, which could cause problem
    GSource* timer_;
    rtc_recorder_t recorder_; // do not free this, it was handled by rtc_manager

    std::map<xmixer_t, MixerRecordingInfo> recordings_;
};

static
rtc_endpoint_callbacks g_mixer_manager_rtc_callback = {
        .context = nullptr,
        .on_destroy = &MixerManager::on_rtc_destroy,
        .on_stream_data = nullptr,
        .on_event = nullptr
};

void MixerManager::on_rtc_destroy(rtc_endpoint_t rtc, void* context) {
    dbgi("MixerManager::on_rtc_destroy");
    auto mm = static_cast<MixerManager*>(context);
    dbgi("dettached rtc |%s|", rtc_endpoint_get_id(rtc));
    mm->rtc_to_mixer_.erase(rtc);
    dbgi("MixerManager::on_rtc_destroy end");
}

gboolean MixerManager::on_tick(gpointer mixer_manager) {
    auto mm = static_cast<MixerManager*>(mixer_manager);
    auto it = mm->mixers_.begin();
    while (it != mm->mixers_.end()) {
        auto id = it->first;
        auto mixer = it->second;
        bool mixed = xmixer_maybe_mix_frame(mixer);
        if (!mixed) {
            dbgi("deleted mixer |%s|", id.c_str());
            xmixer_delete(mixer);
            it = mm->mixers_.erase(it);

            auto recordingInfoIt = mm->recordings_.find(mixer);
            if (recordingInfoIt != mm->recordings_.end()) {
                auto recordingInfo = recordingInfoIt->second;
                rtc_recorder_close_segment(mm->recorder_, recordingInfo.sceneName, recordingInfo.sourceName, recordingInfo.segmentName);
                rtc_recorder_delete_scene(mm->recorder_, recordingInfo.sceneName);
                rtc_recorder_on_internal_source_closed(mm->recorder_, recordingInfo.internalSourceName);
                mm->recordings_.erase(recordingInfoIt);
            }
        } else {
            ++it;
        }
    }
    return TRUE;
}

void* mixer_manager_new(GMainContext* main_context, rtc_recorder_t recorder) {
    return new MixerManager(main_context, recorder);
}

void mixer_manager_delete(void* m) {
    delete static_cast<MixerManager*>(m);
}

int init_recording(xmixer_t mixer, rtc_recorder_t rec, const std::string& output_dir, const std::string& ctx, MixerRecordingInfo& info);

int mixer_manager_add_rtc(void* mix_manager, rtc_endpoint_t rtc, const std::string &mixer_id, bool is_recording,
                          const std::string& out_path, const std::string& ctx, bool& is_mixer_new, std::string& link_addr) {
    auto mm = static_cast<MixerManager*>(mix_manager);

    if (mm->rtc_to_mixer_.count(rtc)) {
        return mixer_manager_error_already_added;
    }
    xmixer_t mixer = nullptr;
    auto mixer_it = mm->mixers_.find(mixer_id);
    if (mixer_it == mm->mixers_.end()) {
        dbgi("mixer |%s| created", mixer_id.c_str());
        is_mixer_new = true;
        mixer = xmixer_create(mixer_id.c_str(), false, app_config_get()->enable_mixer_debug, link_addr);
        if (is_recording) {
            MixerRecordingInfo info;
            int err = init_recording(mixer, mm->recorder_, out_path, ctx, info);
            if (err == 0) {
                dbgi("mixer |%s| init record |%s/%s/%s|", mixer_id.c_str(), info.sceneName.c_str(), info.sourceName.c_str(), info.segmentName.c_str());
                mm->recordings_.emplace(mixer, info);
            }
        }
    } else {
        is_mixer_new = false;
        mixer = mixer_it->second;
        if (is_recording) {
            auto recordingInfoIt = mm->recordings_.find(mixer);
            if (recordingInfoIt != mm->recordings_.end()) {
                auto recordingInfo = recordingInfoIt->second;
                int err = rtc_recorder_update_ctx(mm->recorder_, recordingInfo.sceneName, ctx);
                if (err) {
                    dbge("mixer |%s| could not update recorder ctx for scene |%s|", mixer_id.c_str(), recordingInfo.sceneName.c_str());
                }
            } else {
                MixerRecordingInfo info;
                int err = init_recording(mixer, mm->recorder_, out_path, ctx, info);
                if (err == 0) {
                    dbgi("mixer |%s| init record |%s/%s/%s|", mixer_id.c_str(), info.sceneName.c_str(), info.sourceName.c_str(), info.segmentName.c_str());
                    mm->recordings_.emplace(mixer, info);
                }
            }
        }
    }
    mm->mixers_.emplace(mixer_id, mixer);
    mm->rtc_to_mixer_.emplace(rtc, mixer);
    dbgi("attached rtc |%s| to mixer |%s|", rtc_endpoint_get_id(rtc), mixer_id.c_str());
    int err = xmixer_add_rtc(mixer, rtc);
    if (err) {
        return mixer_manager_error_add_failed;
    }
    rtc_endpoint_register_callback(rtc, mm, &g_mixer_manager_rtc_callback);

    return 0;
}

std::string mixer_manager_strerr(int err) {
    switch (err) {
        case mixer_manager_error_add_failed:
            return "add failed";
        case mixer_manager_error_already_added:
            return "already added";
    }
    return "success";
}

void *mixer_manager_get_mixer(void* mm, const std::string& mixer_id) {
    auto manager = static_cast<MixerManager*>(mm);

    auto it = manager->mixers_.find(mixer_id);

    if (it == manager->mixers_.end()) {
        return nullptr;
    }

    auto mixer = it->second;

    return mixer;
}

int init_recording(xmixer_t mixer, rtc_recorder_t rec, const std::string &output_dir, const std::string &ctx,
                   MixerRecordingInfo &info) {

    std::string mixer_id(xmixer_get_id(mixer));
    std::string scene_name;
    int err = rtc_recorder_new_scene(rec, scene_name, 3, "movie", ctx, 300000, output_dir);
    if (err) {
        dbge("mixer |%s| could not be recorded: failed to create scene", mixer_id.c_str());
        return -1;
    }
    std::ostringstream fmt;
    fmt << "audio-mixer-" << mixer_id;
    std::string mixer_name(fmt.str());
    std::string tlv_pathname;
    xmixer_get_tlv_filename(mixer, tlv_pathname);
    if (tlv_pathname.empty()) {
        err = rtc_recorder_get_tlv_filename(rec, scene_name, mixer_name, tlv_pathname);
        if (err) {
            dbge("mixer |%s| could not be recorded: failed to decide tlv filename", mixer_id.c_str());
            return -2;
        }
        xmixer_set_tlv_filename(mixer, tlv_pathname);
    }

    std::string source_name("-");
    std::string seg_name;
    err = rtc_recorder_open_segment(rec, scene_name, source_name, seg_name, tlv_pathname,
                                    mixer_name, true, false);
    if (err) {
        dbge("mixer |%s| could not be recorded: failed to start segment", mixer_id.c_str());
        rtc_recorder_delete_scene(rec, scene_name);
        return -3;
    }

    info.sceneName = scene_name;
    info.sourceName = source_name;
    info.internalSourceName = mixer_name;
    info.segmentName = seg_name;

    return 0;
}
